/*
 Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights
 reserved.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License, version 2.0,
 as published by the Free Software Foundation.

 This program is also distributed with certain software (including
 but not limited to OpenSSL) that is licensed under separate terms,
 as designated in a particular file or component or in included license
 documentation.  The authors of MySQL hereby grant you an additional
 permission to link the program and your derivative works with the
 separately licensed software that they have included with MySQL.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License, version 2.0, for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 02110-1301  USA
*/

"use strict";

var stats = {
	"TableHandlerFactory" : 0,
	"TableHandler" : {
		"success"    : 0,
		"idempotent" : 0,
		"cache_hit"  : 0
	}
};

var util           = require("util"),
    mynode         = require("./mynode.js"),
    DBTableHandler = require(mynode.common.DBTableHandler).DBTableHandler,
    apiSession     = require("./Session.js"),
    sessionFactory = require("./SessionFactory.js"),
    query          = require("./Query.js"),
    spi            = require(mynode.spi),
    udebug         = unified_debug.getLogger("UserContext.js"),
    stats_module   = require(mynode.api.stats);

stats_module.register(stats, "api", "UserContext");

function Promise() {
  // implement Promises/A+ http://promises-aplus.github.io/promises-spec/
  // until then is called, this is an empty promise with no performance impact
}

function emptyFulfilledCallback(result) {
  return result;
}
function emptyRejectedCallback(err) {
  throw err;
}
/** Fulfill or reject the original promise via "The Promise Resolution Procedure".
 * original_promise is the Promise from this implementation on which "then" was called
 * new_promise is the Promise from this implementation returned by "then"
 * if the fulfilled or rejected callback provided by "then" returns a promise, wire the new_result (thenable)
 *  to fulfill the new_promise when new_result is fulfilled
 *  or reject the new_promise when new_result is rejected
 * otherwise, if the callback provided by "then" returns a value, fulfill the new_promise with that value
 * if the callback provided by "then" throws an Error, reject the new_promise with that Error
 */
var thenPromiseFulfilledOrRejected = function(original_promise, fulfilled_or_rejected_callback, new_promise, result, isRejected) {
  var new_result;
  try {
    if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected before'); }
    if (fulfilled_or_rejected_callback) {
      new_result = fulfilled_or_rejected_callback.call(undefined, result);
    } else {
      if (isRejected) {
        // 2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason.
        new_promise.reject(result);
      } else {
        // 2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value.
        new_promise.fulfill(result);
      }
      return;
    }
    if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected after', new_result); }
    var new_result_type = typeof new_result;
    if ((new_result_type === 'object' && new_result_type != null) | new_result_type === 'function') { 
      // 2.3.3 if result is an object or function
      // 2.3 The Promise Resolution Procedure
      // 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
      if (new_result === original_promise) {
        throw new Error('TypeError: Promise Resolution Procedure 2.3.1');
      }
      // 2.3.2 If x is a promise, adopt its state; but we don't care since it's also a thenable
      var then;
      try {
        then = new_result.then;
      } catch (thenE) {
        // 2.2.3.2 If retrieving the property x.then results in a thrown exception e, 
        // reject promise with e as the reason.
        new_promise.reject(thenE);
        return;
      }
      if (typeof then === 'function') {
        // 2.3.3.3 If then is a function, call it with x as this, first argument resolvePromise, 
        // and second argument rejectPromise
        // 2.3.3.3.3 If both resolvePromise and rejectPromise are called, 
        // or multiple calls to the same argument are made, the first call takes precedence, 
        // and any further calls are ignored.
        try {
          then.call(new_result,
            // 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
            function(result) {
            if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected deferred fulfill callback', new_result); }
              if (!new_promise.resolved) {
                new_promise.fulfill(result);
              }
            },
            // 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
            function(err) {
              if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected deferred reject callback', new_result); }
              if (!new_promise.resolved) {
                new_promise.reject(err);
              }
            }
          );
        } catch (callE) {
          // 2.3.3.3.4 If calling then throws an exception e,
          // 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
          if (!new_promise.resolved) {
            // 2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
            new_promise.reject(callE);
          }
        }
      } else {
        // 2.3.3.4 If then is not a function, fulfill promise with x.
        new_promise.fulfill(new_result);
      }
    } else {
      // 2.3.4 If x is not an object or function, fulfill promise with x.
      new_promise.fulfill(new_result);
    }
  } catch (fulfillE) {
    // 2.2.7.2 If either onFulfilled or onRejected throws an exception e,
    // promise2 must be rejected with e as the reason.
    new_promise.reject(fulfillE);
  }
  
};

Promise.prototype.then = function(fulfilled_callback, rejected_callback, progress_callback) {
  var self = this;
  if (!self) udebug.log('Promise.then called with no this');
  // create a new promise to return from the "then" method
  var new_promise = new Promise();
  if (typeof self.fulfilled_callbacks === 'undefined') {
    self.fulfilled_callbacks = [];
    self.rejected_callbacks = [];
    self.progress_callbacks = [];
  }
  if (self.resolved) {
    var resolved_result;
    if(udebug.is_detail()) { udebug.log(this.name, 'UserContext.Promise.then resolved; err:', self.err); }
    if (self.err) {
      // this promise was already rejected
      if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling (delayed) rejected_callback', rejected_callback); }
      global.setImmediate(function() {
        if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling rejected_callback', fulfilled_callback); }
        thenPromiseFulfilledOrRejected(self, rejected_callback, new_promise, self.err, true);
      });
    } else {
      // this promise was already fulfilled, possibly with a null or undefined result
      if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling (delayed) fulfilled_callback', fulfilled_callback); }
      global.setImmediate(function() {
        if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling fulfilled_callback', fulfilled_callback); }
        thenPromiseFulfilledOrRejected(self, fulfilled_callback, new_promise, self.result);
      });
    }
    return new_promise;
  }
  // create a closure for each fulfilled_callback
  // the closure is a function that when called, calls setImmediate to call the fulfilled_callback with the result
  if (typeof fulfilled_callback === 'function') {
    if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with fulfilled_callback', fulfilled_callback); }
    // the following function closes (this, fulfilled_callback, new_promise)
    // and is called asynchronously when this promise is fulfilled
    this.fulfilled_callbacks.push(function(result) {
      global.setImmediate(function() {
        thenPromiseFulfilledOrRejected(self, fulfilled_callback, new_promise, result);
      });
    });
  } else {
    if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with no fulfilled_callback'); }
    // create a dummy function for a missing fulfilled callback per 2.2.7.3 
    // If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value.
    this.fulfilled_callbacks.push(function(result) {
      global.setImmediate(function() {
        thenPromiseFulfilledOrRejected(self, emptyFulfilledCallback, new_promise, result);
      });
    });
  }

  // create a closure for each rejected_callback
  // the closure is a function that when called, calls setImmediate to call the rejected_callback with the error
  if (typeof rejected_callback === 'function') {
    if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with rejected_callback', rejected_callback); }
    this.rejected_callbacks.push(function(err) {
      global.setImmediate(function() {
        thenPromiseFulfilledOrRejected(self, rejected_callback, new_promise, err);
      });
    });
  } else {
    if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with no rejected_callback');  }
    // create a dummy function for a missing rejected callback per 2.2.7.4 
    // If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason.
    this.rejected_callbacks.push(function(err) {
      global.setImmediate(function() {
        thenPromiseFulfilledOrRejected(self, emptyRejectedCallback, new_promise, err);
      });
    });
  }
  // todo: progress_callbacks
  if (typeof progress_callback === 'function') {
    this.progress_callbacks.push(progress_callback);
  }

  return new_promise;
};

Promise.prototype.fulfill = function(result) {
  var name = this?this.name: 'no this';
  if (udebug.is_detail()) {
    udebug.log_detail(new Error(name, 'Promise.fulfill').stack);
  }
  if (this.resolved) {
    throw new Error('Fatal User Exception: fulfill called after fulfill or reject');
  }
  if(udebug.is_detail()) { 
    udebug.log(name, 'Promise.fulfill with result', result, 'fulfilled_callbacks length:', 
      this.fulfilled_callbacks?  this.fulfilled_callbacks.length: 0); 
  }
  this.resolved = true;
  this.result = result;
  var fulfilled_callback;
  if (this.fulfilled_callbacks) {
    while(this.fulfilled_callbacks.length > 0) {
      fulfilled_callback = this.fulfilled_callbacks.shift();
      if(udebug.is_detail()) { udebug.log('Promise.fulfill for', result); }
      fulfilled_callback(result);
    }
  }
};

Promise.prototype.reject = function(err) {
  if (this.resolved) {
    throw new Error('Fatal User Exception: reject called after fulfill or reject');
  }
  var name = this?this.name: 'no this';
  if(udebug.is_detail()) {
    udebug.log(name, 'Promise.reject with err', err, 'rejected_callbacks length:', 
      this.rejected_callbacks?  this.rejected_callbacks.length: 0);
  }
  this.resolved = true;
  this.err = err;
  var rejected_callback;
  if (this.rejected_callbacks) {
    while(this.rejected_callbacks.length > 0) {
      rejected_callback = this.rejected_callbacks.shift();
      if(udebug.is_detail()) { udebug.log('Promise.reject for', err); }
      rejected_callback(err);
    }
  }
//  throw err;
};

/** Create a function to manage the context of a user's asynchronous call.
 * All asynchronous user functions make a callback passing
 * the user's extra parameters from the original call as extra parameters
 * beyond the specified parameters of the call. For example, the persist function
 * is specified to take two parameters: the data object itself and the callback.
 * The result of persist is to call back with parameters of an error object, 
 * and the same data object which was passed. 
 * If extra parameters are passed to the persist function, the user's function
 * will be called with the specified parameters plus all extra parameters from
 * the original call. 
 * The constructor remembers the original user callback function and the original
 * parameters of the function.
 * The user callback function is always the last required parameter of the function call.
 * Additional context is added as the function progresses.
 * @param user_arguments the original arguments as supplied by the user
 * @param required_parameter_count the number of required parameters 
 * NOTE: the user callback function must be the last of the required parameters
 * @param returned_parameter_count the number of required parameters returned to the callback
 * @param session the Session which may be null for SessionFactory functions
 * @param session_factory the SessionFactory which may be null for Session functions
 * @param execute (optional; defaults to true) whether to execute the operation immediately;
 *        if execute is false, the operation is constructed and is available via the "operation"
 *        property of the user context.
 */
exports.UserContext = function(user_arguments, required_parameter_count, returned_parameter_count,
    session, session_factory, execute) {
  this.execute = (typeof execute === 'boolean' ? execute : true);
  this.user_arguments = user_arguments;
  this.user_callback = user_arguments[required_parameter_count - 1];
  this.required_parameter_count = required_parameter_count;
  this.extra_arguments_count = user_arguments.length - required_parameter_count;
  this.returned_parameter_count = returned_parameter_count;
  this.session = session;
  this.session_factory = session_factory;
  /* indicates that batch.clear was called before this context had executed */
  this.clear = false;
  if (this.session) {
    this.autocommit = ! this.session.tx.isActive();
  }
  this.errorMessages = '';
  this.promise = new Promise();
};

exports.UserContext.prototype.appendErrorMessage = function(message) {
  this.errorMessages += '\n' + message;
};

/** Get table metadata.
 * Delegate to DBConnectionPool.getTableMetadata.
 */
exports.UserContext.prototype.getTableMetadata = function() {
  var userContext = this;
  var err, databaseName, tableName, dbSession;
  var getTableMetadataOnTableMetadata = function(metadataErr, tableMetadata) {
    udebug.log('UserContext.getTableMetadata.getTableMetadataOnTableMetadata with err', metadataErr);
    userContext.applyCallback(metadataErr, tableMetadata);
  };
  databaseName = userContext.user_arguments[0];
  tableName = userContext.user_arguments[1];
  dbSession = (userContext.session)?userContext.session.dbSession:null;
  if (typeof databaseName !== 'string' || typeof tableName !== 'string') {
    err = new Error('getTableMetadata(databaseName, tableName) illegal argument types (' +
        typeof databaseName + ', ' + typeof tableName + ')');
    userContext.applyCallback(err, null);
  } else {
    this.session_factory.dbConnectionPool.getTableMetadata(
        databaseName, tableName, dbSession, getTableMetadataOnTableMetadata);
  }
  return userContext.promise;
};


/** List all tables in the default database.
 * Delegate to DBConnectionPool.listTables.
 */
exports.UserContext.prototype.listTables = function() {
  var userContext = this;
  var listTablesOnTableList = function(err, tableList) {
    userContext.applyCallback(err, tableList);
  };

  var databaseName = this.user_arguments[0];
  var dbSession = (this.session)?this.session.dbSession:null;
  this.session_factory.dbConnectionPool.listTables(databaseName, dbSession, listTablesOnTableList);
  return userContext.promise;
};

/** Create schema from a table mapping. 
 * promise = createTable(tableMapping, callback);
 */
exports.UserContext.prototype.createTable = function() {
  var userContext = this;
  var tableMapping, dbSession;
  function createTableOnTableCreated(err) {
    userContext.applyCallback(err);
  }

  // createTable starts here
  tableMapping = userContext.user_arguments[0];
  userContext.session_factory.dbConnectionPool.createTable(tableMapping, userContext.session, userContext.session_factory,
      createTableOnTableCreated);
  return userContext.promise;
};


/** Resolve properties. Properties might be an object, a name, or null.
 * If null, use all default properties. If a name, use default properties
 * of the named service provider. Otherwise, return the properties object.
 */
var resolveProperties = function(properties) {
  // Properties can be a string adapter name.  It defaults to 'ndb'.
  if(typeof properties === 'string') {
    properties = spi.getDBServiceProvider(properties).getDefaultConnectionProperties();
  }
  else if (properties === null) {
    properties = spi.getDBServiceProvider('ndb').getDefaultConnectionProperties();
  }
  return properties;
};

function getTableSpecification(defaultDatabaseName, tableName) {
  var split = tableName.split('\.');
  var result = {};
  if (split.length == 2) {
    result.dbName = split[0];
    result.unqualifiedTableName = split[1];
    result.qualifiedTableName = tableName;
  } else {
    // if split.length is not 1 then this error will be caught later
    result.dbName = defaultDatabaseName;
    result.unqualifiedTableName = tableName;
    result.qualifiedTableName = defaultDatabaseName + '.' + tableName;
  }
  udebug.log_detail('getTableSpecification for', defaultDatabaseName, ',', tableName, 'returned', result);
  return result;
}
/** Construct the table name from possibly empty database name and table name.
 */
function constructDatabaseDotTable(databaseName, tableName) {
  var result = databaseName ? databaseName + '.' + tableName : tableName;
  return result;
}

/** Get the table handler for a domain object, table name, or constructor.
 */
var getTableHandler = function(domainObjectTableNameOrConstructor, session, onTableHandler) {

  // the table name might be qualified if the mapping specified a qualified table name
  // if unqualified, use sessionFactory.properties.database to qualify the table name
  var TableHandlerFactory = function(mynode, tableSpecification,
      sessionFactory, dbSession, mapping, ctor, onTableHandler) {
    this.sessionFactory = sessionFactory;
    this.dbSession = dbSession;
    this.onTableHandler = onTableHandler;
    this.mapping = mapping;
    this.mynode = mynode;
    this.ctor = ctor;
    this.tableSpecification = tableSpecification;
    stats.TableHandlerFactory++;
    
    this.createTableHandler = function() {
      var tableHandlerFactory = this;
      var tableHandler;
      var tableMetadata;
      var tableMapping;
      
      var onTableMetadata = function(err, tableMetadata) {
        var tableHandler;
        var tableKey = tableHandlerFactory.tableSpecification.qualifiedTableName;
        if(udebug.is_detail()) {
          udebug.log('TableHandlerFactory.onTableMetadata for ',
            tableHandlerFactory.tableSpecification.qualifiedTableName + ' with err: ' + err);
        }
        if (err) {
          tableHandlerFactory.onTableHandler(err, null);
        } else {
          // check to see if the metadata has already been processed
          if (typeof tableHandlerFactory.sessionFactory.tableMetadatas[tableKey] === 'undefined') {
            // put the table metadata into the table metadata map
            tableHandlerFactory.sessionFactory.tableMetadatas[tableKey] = tableMetadata;
          }
          // we have the table metadata; now create the table handler if needed
          // put the table handler into the session factory
          if (typeof(tableHandlerFactory.sessionFactory.tableHandlers[tableKey]) === 'undefined') {
            if(udebug.is_detail()) {
              udebug.log('UserContext caching the table handler in the sessionFactory for ', 
                tableHandlerFactory.tableName);
            }
            tableHandler = new DBTableHandler(tableMetadata, tableHandlerFactory.mapping,
                tableHandlerFactory.ctor);
            tableHandlerFactory.sessionFactory.tableHandlers[tableKey] = tableHandler;
          } else {
            tableHandler = tableHandlerFactory.sessionFactory.tableHandlers[tableKey];
            if(udebug.is_detail()) {
              udebug.log('UserContext got tableHandler but someone else put it in the cache first for ', 
                tableHandlerFactory.tableName);
            }
          }
          if (tableHandlerFactory.ctor) {
            if (typeof(tableHandlerFactory.ctor.prototype.mynode.tableHandler) === 'undefined') {
              // if a domain object mapping, cache the table handler in the prototype
              stats.TableHandler.success++;
              tableHandler = new DBTableHandler(tableMetadata, tableHandlerFactory.mapping,
                  tableHandlerFactory.ctor);
              if (tableHandler.isValid) {
                tableHandlerFactory.ctor.prototype.mynode.tableHandler = tableHandler;
                if(udebug.is_detail()) {
                  udebug.log('UserContext caching the table handler in the prototype for constructor.');
                }
              } else {
                tableHandlerFactory.err = tableHandler.err;
                if(udebug.is_detail()) { udebug.log('UserContext got invalid tableHandler', tableHandler.errorMessages); }
              }
            } else {
              tableHandler = tableHandlerFactory.ctor.prototype.mynode.tableHandler;
              stats.TableHandler.idempotent++;
              if(udebug.is_detail()) {
                udebug.log('UserContext got tableHandler but someone else put it in the prototype first.');
              }
            }
          }
          tableHandlerFactory.onTableHandler(tableHandlerFactory.err, tableHandler);
        }
      };

      function tableHandlerFactoryOnCreateTable(err) {
        if (err) {
          onTableMetadata(err, null);
        } else {
          sessionFactory.dbConnectionPool.getTableMetadata(tableHandlerFactory.tableSpecification.dbName,
              tableHandlerFactory.tableSpecification.unqualifiedTableName, session.dbSession, onTableMetadata);
        }
      }
      
      // start of createTableHandler
      
      // get the table metadata from the cache of table metadatas in session factory
      tableMetadata = 
        tableHandlerFactory.sessionFactory.tableMetadatas[tableHandlerFactory.tableSpecification.qualifiedTableName];
      if (tableMetadata) {
        // we already have cached the table metadata
        onTableMetadata(null, tableMetadata);
      } else {
        // create the schema if it does not already exist
        tableMapping = sessionFactory.tableMappings[tableSpecification.qualifiedTableName];
        if (tableMapping) {
          udebug.log('tableHandlerFactory.createTableHandler creating schema using tableMapping: ', tableMapping);
          sessionFactory.createTable(tableMapping, tableHandlerFactoryOnCreateTable);
          return;
        }
        // get the table metadata from the db connection pool
        // getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
        udebug.log('TableHandlerFactory.createTableHandler for ', 
            tableHandlerFactory.tableSpecification.dbName,
            tableHandlerFactory.tableSpecification.unqualifiedTableName);
        this.sessionFactory.dbConnectionPool.getTableMetadata(
            tableHandlerFactory.tableSpecification.dbName,
            tableHandlerFactory.tableSpecification.unqualifiedTableName, session.dbSession, onTableMetadata);
      }
    };
  };
    
  // start of getTableHandler 
  var err, mynode, tableHandler, tableMapping, tableHandlerFactory, tableIndicatorType, tableSpecification, databaseDotTable;

  tableIndicatorType = typeof(domainObjectTableNameOrConstructor);
  if (tableIndicatorType === 'string') {
    if(udebug.is_detail()) {
      udebug.log('UserContext.getTableHandler for table ', domainObjectTableNameOrConstructor); 
    }
    tableSpecification = getTableSpecification(session.sessionFactory.properties.database,
        domainObjectTableNameOrConstructor);

    // parameter is a table name; look up in table name to table handler hash
    tableHandler = session.sessionFactory.tableHandlers[tableSpecification.qualifiedTableName];
    if (typeof(tableHandler) === 'undefined') {
      udebug.log('UserContext.getTableHandler did not find cached tableHandler for table ',
          tableSpecification.qualifiedTableName);
      // get a table mapping from session factory
      tableMapping = session.sessionFactory.tableMappings[tableSpecification.qualifiedTableName];
      // create a new table handler for a table name with no mapping
      // create a closure to create the table handler
      tableHandlerFactory = new TableHandlerFactory(
          null, tableSpecification, session.sessionFactory, session.dbSession,
          tableMapping, null, onTableHandler);
      tableHandlerFactory.createTableHandler(null);
    } else {
      if(udebug.is_detail()) {
        udebug.log('UserContext.getTableHandler found cached tableHandler for table ',
          tableSpecification.qualifiedTableName);
      }
      // send back the tableHandler
      onTableHandler(null, tableHandler);
    }
  } else if (tableIndicatorType === 'function') {
    if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler for constructor.'); }
    mynode = domainObjectTableNameOrConstructor.prototype.mynode;
    // parameter is a constructor; it must have been annotated already
    if (typeof(mynode) === 'undefined') {
      err = new Error('User exception: constructor for ' +  domainObjectTableNameOrConstructor.prototype.constructor.name +
      ' must have been annotated (call TableMapping.applyToClass).');
      onTableHandler(err, null);
    } else {
      tableHandler = mynode.tableHandler;
      if (typeof(tableHandler) === 'undefined') {
        udebug.log('UserContext.getTableHandler did not find cached tableHandler for constructor.',
            domainObjectTableNameOrConstructor);
        // create the tableHandler
        // getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
        databaseDotTable = constructDatabaseDotTable(mynode.mapping.database, mynode.mapping.table);
        tableSpecification = getTableSpecification(session.sessionFactory.properties.database, databaseDotTable);
        tableHandlerFactory = new TableHandlerFactory(
            mynode, tableSpecification, session.sessionFactory, session.dbSession, 
            mynode.mapping, domainObjectTableNameOrConstructor, onTableHandler);
        tableHandlerFactory.createTableHandler();
      } else {
        stats.TableHandler.cache_hit++;
        if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler found cached tableHandler for constructor.'); }
        // prototype has been annotated; return the table handler
        onTableHandler(null, tableHandler);
      }
    }
  } else if (tableIndicatorType === 'object') {
    if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler for domain object.'); }
    // parameter is a domain object; it must have been mapped already
    mynode = domainObjectTableNameOrConstructor.constructor.prototype.mynode;
    if (typeof(mynode) === 'undefined') {
      err = new Error('User exception: constructor for ' +  domainObjectTableNameOrConstructor.constructor.name +
          ' must have been annotated (call TableMapping.applyToClass).');
      onTableHandler(err, null);
    } else {
      tableHandler = mynode.tableHandler;
      if (typeof(tableHandler) === 'undefined') {
        if(udebug.is_detail()) {
          udebug.log('UserContext.getTableHandler did not find cached tableHandler for object\n',
                      util.inspect(domainObjectTableNameOrConstructor),
                     'constructor\n', domainObjectTableNameOrConstructor.constructor);
        }
        databaseDotTable = constructDatabaseDotTable(mynode.mapping.database, mynode.mapping.table);
        tableSpecification = getTableSpecification(session.sessionFactory.properties.database, databaseDotTable);
        // create the tableHandler
        // getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
        tableHandlerFactory = new TableHandlerFactory(
            mynode, tableSpecification, session.sessionFactory, session.dbSession, 
            mynode.mapping, domainObjectTableNameOrConstructor.constructor, onTableHandler);
        tableHandlerFactory.createTableHandler();
      } else {
        if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler found cached tableHandler for constructor.'); }
        // prototype has been annotated; return the table handler
        onTableHandler(null, tableHandler);
      }
    }
  } else {
    err = new Error('User error: parameter must be a domain object, string, or constructor function.');
    onTableHandler(err, null);
  }
};

/** Try to find an existing session factory by looking up the connection string
 * and database name. Failing that, create a db connection pool and create a session factory.
 * Multiple session factories share the same db connection pool.
 * This function is used by both connect and openSession.
 */
var getSessionFactory = function(userContext, properties, tableMappings, callback) {
  var database;
  var connectionKey;
  var connection;
  var factory;
  var newSession;
  var sp;
  var i;
  var m;

  var resolveTableMappingsOnSession = function(err, session) {
    var mappings = [];
    var mappingBeingResolved = 0;

    var resolveTableMappingsOnTableHandler = function(err, tableHandler) {
      if(udebug.is_detail()) {
        udebug.log('UserContext.resolveTableMappinsgOnTableHandler', mappingBeingResolved + 1,
                   'of', mappings.length, mappings[mappingBeingResolved]);
      }
      if (err) {
        userContext.appendErrorMessage(err);
      }
      if (++mappingBeingResolved === mappings.length || mappingBeingResolved > 10) {
        // close the session the hard way (not using UserContext)
        session.dbSession.close(function(err) {
          if (err) {
            callback(err, null);
          } else {
            // now remove the session from the session factory's open connections
            session.sessionFactory.closeSession(session.index);
            // mark this session as unusable
            session.closed = true;
            // if any errors during table mapping, report them
            if (userContext.errorMessages) {
              err = new Error(userContext.errorMessages);
              callback(err, null);
            } else {
              // no errors
              callback(null, factory);
            }
          }
        });
      } else {
        // get the table handler for the next one, and so on until all are done
        getTableHandler(mappings[mappingBeingResolved], session, resolveTableMappingsOnTableHandler);
      }
    };

    // resolveTableMappingsOnSession begins here
    
    var tableMappingsType = typeof(tableMappings);
    var tableMapping;
    var tableMappingType;
    switch (tableMappingsType) {
    case 'string': 
      mappings.push(tableMappings); 
      break;
    case 'function': 
      mappings.push(tableMappings);
      break;
    case 'object': 
      if (tableMappings.length) {
        for (m = 0; m < tableMappings.length; ++m) {
          tableMapping = tableMappings[m];
          tableMappingType = typeof(tableMapping);
          if (tableMappingType === 'function' || tableMappingType === 'string') {
            mappings.push(tableMapping);
          } else {
            userContext.appendErrorMessage('unknown table mapping' + util.inspect(tableMapping));
          }
        }
      } else {
        userContext.appendErrorMessage('unknown table mappings' + util.inspect(tableMappings));
      }
      break;
    default:
      userContext.appendErrorMessage('unknown table mappings' + util.inspect(tableMappings));
      break;
    }
    if (mappings.length === 0) {
      if(udebug.is_detail()) { udebug.log('resolveTableMappingsOnSession no mappings!'); }
      callback(null, factory);
    }
    // get table handler for the first; the callback will then do the next one...
    if(udebug.is_detail()) { udebug.log('getSessionFactory resolving mappings:', mappings); }
    getTableHandler(mappings[0], session, resolveTableMappingsOnTableHandler);
  };

  var resolveTableMappingsAndCallback = function() {
    if (!tableMappings) {
      callback(null, factory);
    } else {
      // get a session the hard way (not using UserContext) to resolve mappings
      var sessionSlot = factory.allocateSessionSlot();
      factory.dbConnectionPool.getDBSession(userContext.session_index, function(err, dbSession) {
        if (err) {
          // report error
          userContext.appendErrorMessage(err);
          err = new Error(userContext.errorMessages);
          callback(err, null);          
        } else {
          var newSession = new apiSession.Session(sessionSlot, factory, dbSession);
          factory.sessions[sessionSlot] = newSession;
          resolveTableMappingsOnSession(err, newSession);
        }
      });
    }
  };

  var createFactory = function(dbConnectionPool) {
    var newFactory;
    udebug.log('connect createFactory creating factory for', connectionKey, 'database', database);
    newFactory = new sessionFactory.SessionFactory(connectionKey, dbConnectionPool,
        properties, tableMappings, mynode.deleteFactory);
    return newFactory;
  };
  
  var dbConnectionPoolCreated_callback = function(error, dbConnectionPool) {
    if (connection.isConnecting) {
      // the first requester for this connection
      connection.isConnecting = false;
      if (error) {
        callback(error, null);
      } else {
        udebug.log('dbConnectionPool created for', connectionKey, 'database', database);
        connection.dbConnectionPool = dbConnectionPool;
        factory = createFactory(dbConnectionPool);
        connection.factories[database] = factory;
        connection.count++;
        resolveTableMappingsAndCallback();
      }
      // notify all others that the connection is now ready (or an error was signaled)
      for (i = 0; i < connection.waitingForConnection.length; ++i) {
        if(udebug.is_detail()) { udebug.log('dbConnectionPoolCreated_callback notifying...'); }
        udebug.log('dbConnectionPoolCreated', error, dbConnectionPool);
        connection.waitingForConnection[i](error, dbConnectionPool);
      }
    } else {
      // another user request created the dbConnectionPool and session factory
      if (error) {
        callback(error, null);
      } else {
        udebug.log('dbConnectionPoolCreated_callback', database, connection.factories);
        factory = connection.factories[database];
        if (!factory) {
          factory = createFactory(dbConnectionPool);
          connection.factories[database] = factory;
          connection.count++;
        }
        resolveTableMappingsAndCallback();
      }
    }
  };

  // getSessionFactory starts here
  database = properties.database;
  connectionKey = mynode.getConnectionKey(properties);
  connection = mynode.getConnection(connectionKey);

  if(typeof(connection) === 'undefined') {
    // there is no connection yet using this connection key    
    udebug.log('connect connection does not exist; creating factory for',
               connectionKey, 'database', database);
    connection = mynode.newConnection(connectionKey);
    sp = spi.getDBServiceProvider(properties.implementation);
    sp.connect(properties, dbConnectionPoolCreated_callback);
  } else {
    // there is a connection, but is it already connected?
    if (connection.isConnecting) {
      // wait until the first requester for this connection completes
      udebug.log('connect waiting for db connection by another for', connectionKey, 'database', database);
      connection.waitingForConnection.push(dbConnectionPoolCreated_callback);
    } else {
      // there is a connection, but is there a SessionFactory for this database?
      factory = connection.factories[database];
      if (typeof(factory) === 'undefined') {
        // create a SessionFactory for the existing dbConnectionPool
        udebug.log('connect creating factory with existing', connectionKey, 'database', database);
        factory = createFactory(connection.dbConnectionPool);
        connection.factories[database] = factory;
        connection.count++;
      }
//    resolve all table mappings before returning
      resolveTableMappingsAndCallback();
    }
  }
  
};

exports.UserContext.prototype.connect = function() {
  var userContext = this;
  // properties might be null, a name, or a properties object
  this.user_arguments[0] = resolveProperties(this.user_arguments[0]);

  var connectOnSessionFactory = function(err, factory) {
    userContext.applyCallback(err, factory);
  };

  getSessionFactory(this, this.user_arguments[0], this.user_arguments[1], connectOnSessionFactory);
  return userContext.promise;
};

function checkOperation(err, dbOperation) {
  var sqlstate, message, result, result_code;
  result = null;
  result_code = null;
  message = 'Unknown Error';
  sqlstate = '22000';
  if (err) {
    udebug.log('checkOperation returning existing err:', err);
    return err;
  } 
  if (dbOperation.result.success !== true) {
    if(dbOperation.result.error) {
      sqlstate = dbOperation.result.error.sqlstate;
      message = dbOperation.result.error.message || 'Operation error';
      result_code = dbOperation.result.error.code;
    }
    result = new Error(message);
    result.code = result_code;
    result.sqlstate = sqlstate;
    udebug.log('checkOperation returning new err:', result);
  }
  return result;
}

/** Create a sector object for a domain object in a projection.
 * The topmost outer loop projection's sectors are all created, creating one sector
 * for each inner loop projection, then the outer projection for the next level down.
 * For each inner loop projection, a sector is constructed
 * and then the sector for the included relationship is constructed by recursion.
 * The sector contains a list of primary key fields and a list of non-primary key fields,
 * and if this is not the root sector, the name of the field and the field in the previous sector.
 * The fields are references to the field objects in DBTableHandler and contain names, types,
 * and converters.
 * This function is synchronous. When complete, this function returns to the caller.
 * @param inout parameter: outerLoopProjection the projection at the outer loop
 *   which is modified by this function to add the sectors field
 * @param inout parameter: innerLoopProjection the projection at the inner loop
 *   which is modified by this function to add the sectors field
 * @param sectors the outer loop projection.sectors which will grow as createSector is called recursively
 * @param index the index into sectors for the sector being constructed
 * @param offset the number of fields in all sectors already processed
 */
function createSector(outerLoopProjection, innerLoopProjection, sectors, index, offset) {
  udebug.log('createSector ' + outerLoopProjection.name + ' for ' + outerLoopProjection.domainObject.name +
      ' inner: ' + innerLoopProjection.name + ' for ' + innerLoopProjection.domainObject.name +
      ' index: ' + index + ' offset: ' + offset);
  var projection = innerLoopProjection;
  var innerNestedProjection, outerNestedProjection;
  var sector = {};
  var tableHandler, relationships;
  var keyFieldCount, nonKeyFieldCount;
  var fieldNames, field;
  var indexHandler;
  var relationshipName;
  var relatedFieldMapping, relatedSector, relatedTableHandler, relatedTargetFieldName, relatedTargetField;
  var thisFieldMapping;
  var joinTable, joinTableHandler;
  var foreignKeys, foreignKey, fkIndex, foreignKeyName;
  var i, fkFound;

  // initialize sector
  sector.keyFields = [];
  sector.nonKeyFields = [];
  sector.keyFieldNames = [];
  sector.projection = projection;
  sector.offset = offset;
  tableHandler = projection.domainObject.prototype.mynode.tableHandler;
  udebug.log_detail('createSector for table handler', tableHandler.dbTable.name);
  sector.tableHandler = tableHandler;

  // relatedFieldMapping is the field mapping for the sector to the "left" of this sector
  // it contains the field in the "left" sector and mapping information including join columns
  relatedFieldMapping = projection.relatedFieldMapping;
  sector.relatedFieldMapping = relatedFieldMapping;
  udebug.log_detail('createSector thisDBTable name', tableHandler.dbTable.name, index, sectors);
  if (relatedFieldMapping && index !== 0) {
    // only perform related field mapping for nested projections
    relatedSector = sectors[index - 1];
    relatedTableHandler = relatedSector.tableHandler;
    sector.relatedTableHandler = relatedTableHandler;
    // get this optional field mapping that corresponds to the related field mapping
    // it may be needed to find the foreign key or join table
    relatedTargetFieldName = relatedFieldMapping.targetField;
    relatedTargetField = sector.tableHandler.fieldNameToFieldMap[relatedTargetFieldName];
    if (relatedFieldMapping.toMany && relatedFieldMapping.manyTo) {
      // this is a many-to-many relationship using a join table
      joinTable = relatedFieldMapping.joinTable;
      // joinTableHandler is the DBTableHandler for the join table resolved during validateProjection
      if (joinTable) {
        // join table is defined on the related side
        joinTableHandler = relatedFieldMapping.joinTableHandler;
      } else {
        // join table must be defined on this side
        thisFieldMapping = tableHandler.fieldNameToFieldMap[relatedFieldMapping.targetField];
        joinTable = thisFieldMapping.joinTable;
        if (!joinTable) {
          // error; neither side defined the join table
          projection.error += '\nMappingError: ' + relatedTableHandler.newObjectConstructor.name +
            ' field ' + relatedFieldMapping.fieldName + ' neither side defined the join table.';
        }
        joinTableHandler = thisFieldMapping.joinTableHandler;
      }
      sector.joinTableHandler = joinTableHandler;
      // many to many relationship has a join table with at least two foreign keys; 
      // one to each table mapped to the two domain objects
      if (joinTable) {
        for (foreignKeyName in joinTableHandler.foreignKeyMap) {
          if (joinTableHandler.foreignKeyMap.hasOwnProperty(foreignKeyName)) {
            foreignKey = joinTableHandler.foreignKeyMap[foreignKeyName];
            // is this foreign key for this table?
            if (foreignKey.targetDatabase === tableHandler.dbTable.database && 
                foreignKey.targetTable === tableHandler.dbTable.name) {
              // this foreign key is for the other table
              relatedFieldMapping.otherForeignKey = foreignKey;
            }
            if (foreignKey.targetDatabase === relatedTableHandler.dbTable.database && 
                foreignKey.targetTable === relatedTableHandler.dbTable.name) {
              relatedFieldMapping.thisForeignKey = foreignKey;
            }
          }
        }
        if (!(relatedFieldMapping.thisForeignKey && relatedFieldMapping.otherForeignKey)) {
          // error must have foreign keys to both this table and related table
          projection.error += '\nMappingError: ' + relatedTableHandler.newObjectConstructor.name +
          ' field ' + relatedFieldMapping.fieldName + ' join table must include foreign keys for both sides.';
        }
      }
    } else {
      // this is a relationship using a foreign key
      // resolve the columns involved in the join to the related field
      // there is either a foreign key or a target field that has a foreign key
      // the related field mapping is the field mapping on the other side
      // the field mapping on this side is not used in this projection
      foreignKeyName = relatedFieldMapping.foreignKey;
      if (foreignKeyName) {
        // foreign key is defined on the other side
        foreignKey = relatedTableHandler.getForeignKey(foreignKeyName);
        sector.thisJoinColumns = foreignKey.targetColumnNames;
        sector.otherJoinColumns = foreignKey.columnNames;
      } else {
        // foreign key is defined on this side
        // get the fieldMapping for this relationship field
        relatedTargetField = sector.tableHandler.fieldNameToFieldMap[relatedTargetFieldName];
        foreignKeyName = relatedTargetField.foreignKey;
        if (foreignKeyName) {
        foreignKey = tableHandler.getForeignKey(foreignKeyName);
        sector.thisJoinColumns = foreignKey.columnNames;
        sector.otherJoinColumns = foreignKey.targetColumnNames;
        } else {
          // error: neither side defined the foreign key
          projection.error += 'MappingError: ' + relatedTableHandler.newObjectConstructor.name +
            ' field ' + relatedFieldMapping.fieldName + ' neither side defined the foreign key.';
        }
      }
    }
  }
  // create key fields from primary key index handler
  indexHandler = tableHandler.dbIndexHandlers[0];
  keyFieldCount = indexHandler.fieldNumberToFieldMap.length;
  sector.keyFieldCount = keyFieldCount;
  for (i = 0; i < keyFieldCount; ++i) {
    field = indexHandler.fieldNumberToFieldMap[i];
    sector.keyFields.push(field);
    sector.keyFieldNames.push(field.fieldName);
  }
  // create non-key fields from projection fields excluding key fields
  fieldNames = projection.fields;
  fieldNames.forEach(function(fieldName) {
    // is this field in key fields?
    if (sector.keyFieldNames.indexOf(fieldName) == -1) {
      // non-key field; add it to non-key fields
      field = tableHandler.fieldNameToFieldMap[fieldName];
      sector.nonKeyFields.push(field);
    }
  });
  sector.nonKeyFieldCount = sector.nonKeyFields.length;
  udebug.log_detail('createSector created new sector for index', index, 'sector', sector);
  sectors.push(sector);
  
  innerNestedProjection = projection.firstNestedProjection;
  if (innerNestedProjection) {
    // recurse at the same outer loop, creating the next sector for the inner loop projection
    createSector(outerLoopProjection, innerNestedProjection,
        sectors, index + 1, offset + keyFieldCount + sector.nonKeyFieldCount);
  } else {
    // we are done at this outer projection level; 
    if (outerLoopProjection.name) {
      udebug.log('createSector for ' + outerLoopProjection.name +
          ' created ' + outerLoopProjection.sectors.length + ' sectors for ' + outerLoopProjection.domainObject.name);
    }
    // now go to the outer projection next level down and do it all over again
    outerNestedProjection = outerLoopProjection.firstNestedProjection;
    if (outerNestedProjection) {
      outerNestedProjection.sectors = [];
      createSector(outerNestedProjection, outerNestedProjection, outerNestedProjection.sectors, 0, 0);
    }
  }
}


/** Mark all projections reachable from this projection as validated. */
function markValidated(projections) {
  var projection, relationships, relationshipName;
  if (projections.length > 0) {
    // "pop" the top projection
    projection = projections.shift();
    // mark the top projection validated
    projection.validated = true;
    // if any relationships, add them to the list of projections to validate
    relationships = projection.relationships;
    if (relationships) {
      for (relationshipName in relationships) {
        if (relationships.hasOwnProperty(relationshipName)) {
          projections.push(relationships[relationshipName]);
        }
      }
    }
    // recursively mark related projections
    markValidated(projections);
  }
}

/** Collect errors from all projections reachable from this projection */
function collectErrors(projections, errors) {
  var projection, relationships, relationshipName;
  if (projections.length > 0) {
    // "pop" the top projection
    projection = projections.shift();
    // check the top projection for errors
    errors += projection.error;
    // if any relationships, add them to the list of projections to validate
    relationships = projection.relationships;
    if (relationships) {
      for (relationshipName in relationships) {
        if (relationships.hasOwnProperty(relationshipName)) {
          projections.push(relationships[relationshipName]);
        }
      }
    }
  } else {
    return errors;
  }
  return collectErrors(projections, errors);
}

/** Validate the projection for find and query operations on the domain object.
 * this.user_arguments[0] contains the projection for this operation
 * (first parameter of find or createQuery).
 * Validation occurs in two phases. The first phase individually validates 
 * each domain object associated with a projection. The second phase,
 * implemented as createSector, validates relationships among the domain objects.
 * 
 * In the first phase, get the table handler for the domain object and validate
 * that it is mapped. Then validate each field in the projection.
 * For relationships, validate the name of the relationship. Validate that there
 * is no projected domain object that would cause an infinite recursion.
 * If there is a join table that implements the relationship, validate that the
 * join table exists by loading its metadata.
 * Store the field mapping for this relationship in the related projection.
 * The related field mapping will be further processed in the second phase,
 * once the table metadata for both domain objects has been loaded.
 * Recursively validate the projection that is defined as the relationship.
 * 
 * In the second phase, validate that the relationship is mapped with valid
 * foreign keys and join tables in the database.
 * After all projections have been validated, call the callback with any errors.
 */
exports.UserContext.prototype.validateProjection = function(callback) {
  var userContext = this;
  var session = userContext.session;
  var err;
  var domainObject, domainObjectName;
  var projections, projection;
  var mappingIds, mappingId;
  var relationships, relationshipProjection;
  var fieldMapping;
  var index, offset;
  var errors;
  var foreignKeyNames, foreignKeyName;
  var toBeChecked, toBeValidated, allValidated;
  var domainObjectMynode;
  var joinTableRelationshipField, joinTableRelationshipFields = [];
  var continueValidation;

  function validateJoinTableOnTableHandler(err, joinTableHandler) {
    udebug.log_detail('validateJoinTableOnTableHandler for', joinTableRelationshipField.joinTable, 'err:', err);
    if (err) {
      // mark the projection as broken
      errors += '\nBad projection for ' +  domainObjectName + ': field ' + joinTableRelationshipField.fieldName +
        ' join table ' + joinTableRelationshipField.joinTable + ' failed: ' + err.message;
    } else {
      // continue validating projections
      // we cannot do any more until both sides have their table handlers
      udebug.log_detail('validateJoinTableOnTableHandler resolved table handler for ', domainObjectName,
          ': field', joinTableRelationshipField.fieldName,
          'join table', joinTableRelationshipField.joinTable);
      // store the join table handler in the related field mapping
      joinTableRelationshipField.joinTableHandler = joinTableHandler;
    }
    // finished this join table; continue with more join tables or more tables mapped to domain objects
    joinTableRelationshipField = joinTableRelationshipFields.shift();
    if (joinTableRelationshipField) {
      getTableHandler(joinTableRelationshipField.joinTable, session, validateJoinTableOnTableHandler);
    } else {
      continueValidation();
    }
  }

  function validateProjectionOnTableHandler(err, dbTableHandler) {
    // currently validating projections[index] with the tableHandler for the domain object
    projection = projections[index];
    // keep track of how many times this projection has been changed so adapters know when to re-validate
    projection.id = (projection.id + 1) % (2^24);
    
    domainObject = projection.domainObject;
    domainObjectName = domainObject.prototype.constructor.name;
    domainObjectMynode = domainObject.prototype.mynode;
    if (domainObjectMynode && domainObjectMynode.mapping.error) {
      // remember errors in mapping
      errors += domainObjectMynode.mapping.error;
    }
    if (!err) {
      projection.dbTableHandler = dbTableHandler;
      // validate using table handler
      if (typeof(domainObject) === 'function' &&
          typeof(domainObject.prototype.mynode) === 'object' &&
          typeof(domainObject.prototype.mynode.mapping) === 'object') {
        // good domainObject; have we seen this one before?
        mappingId = domainObject.prototype.mynode.mappingId;
        if (mappingIds.indexOf(mappingId) === -1) {
          // have not seen this one before; add its mappingId to list of mappingIds to prevent cycles (recursion)
          mappingIds.push(mappingId);
          // validate all fields in projection are mapped
          if (projection.fields) { // field names
            projection.fields.forEach(function(fieldName) {
              fieldMapping = dbTableHandler.fieldNameToFieldMap[fieldName];
              if (fieldMapping) {
                if (fieldMapping.relationship) {
                  errors += '\nBad projection for ' +  domainObjectName + ': field' + fieldName + ' must not be a relationship';
                }
              } else {
                // error: fields must be mapped
                errors += '\nBad projection for ' +  domainObjectName + ': field ' + fieldName + ' is not mapped';
              }
            });
          }
          // validate all relationships in mapping regardless of whether they are in this projection
          
          dbTableHandler.relationshipFields.forEach(function(relationshipField) {
            // get the name and projection for each relationship
            foreignKeyName = relationshipField.foreignKey;
            if (foreignKeyName) {
              // make sure the foreign key exists
              if (!dbTableHandler.foreignKeyMap.hasOwnProperty(foreignKeyName)) {
                errors += '\nBad relationship field mapping; foreign key ' + foreignKeyName +
                    ' does not exist in table; possible foreign keys are: ' + Object.keys(dbTableHandler.foreignKeyMap);
              }
            }
            // remember this relationship in order to resolve table mapping for join table
            if (relationshipField.joinTable) {
              joinTableRelationshipFields.push(relationshipField);
            }
          });
          // add relationship domain objects to the list of domain objects
          relationships = projection.relationships;
          if (relationships) {
            Object.keys(relationships).forEach(function(key) {
              // each key is the name of a relationship that must be a field in the table handler
              fieldMapping = dbTableHandler.fieldNameToFieldMap[key];
              if (fieldMapping) {
                if (fieldMapping.relationship) {
                  relationshipProjection = relationships[key];
                  relationshipProjection.relatedFieldMapping = fieldMapping;
                  // add each relationship to list of projections
                  projections.push(relationshipProjection);
                  // record the first (currently the only) projection that is nested in this projection
                  if (!projection.firstNestedProjection) {
                    projection.firstNestedProjection = relationshipProjection;
                  }
                } else {
                  // error: field is not a relationship
                  errors += '\nBad relationship for ' +  domainObjectName + ': field ' + key + ' is not a relationship.';
                }
              } else {
                // error: relationships must be mapped
                errors += '\nBad relationship for ' +  domainObjectName + ': field ' + key + ' is not mapped.';
              }
            });
          }
        } else {
          // recursive projection
          errors += '\nRecursive projection for ' + domainObjectName;
        }
      } else {
        // domainObject was not mapped
        errors += '\nBad domain object: ' + domainObjectName + ' is not mapped.';
      } 
    } else {
      // table does not exist
        errors += '\nUnable to acquire tableHandler for ' + domainObjectName + ' : ' + err.message;
    }
    // finished validating this projection; do we have a join table to validate?
    if (joinTableRelationshipFields.length > 0) {
      // get the table handler for the first join table
      joinTableRelationshipField = joinTableRelationshipFields.shift();
      getTableHandler(joinTableRelationshipField.joinTable, session, validateJoinTableOnTableHandler);
    } else {
      continueValidation();
    }
  }
  
  // continue validation from either projection domain object or relationship join table
  continueValidation = function() {
    // are there any more?
    if (projections.length > ++index) {
      // do the next projection
      if (projections[index].domainObject.prototype.mynode.dbTableHandler) {
        udebug.log('validateProjection with cached tableHandler for', projections[index].domainObject.name);
        validateProjectionOnTableHandler(null, projections[index].domainObject.prototype.mynode.dbTableHandler);
      } else {
        udebug.log('validateProjection with no cached tableHandler for', projections[index].domainObject.name);
        getTableHandler(projections[index].domainObject, session, validateProjectionOnTableHandler);
      }
    } else {
      // there are no more projections to validate -- did another user finish table handling first?
      if (!userContext.user_arguments[0].validated) {
        // we are the first to validate table handling -- check for errors
        if (!errors) {
          projection = projections[0];
          // no errors yet
          // we are done getting all of the table handlers for the projection; now create the sectors
          projection.sectors = [];
          index = 0;
          offset = 0;
          // create the first sector; additional sectors will be created recursively
          createSector(projection, projection, projection.sectors, index, offset);
          // now look for errors found during createSector
          errors = collectErrors([userContext.user_arguments[0]], '');
          // mark all projections reachable from this projections as validated
          // projections will grow at the end as validated marking proceeds
          if (!errors) {
            // no errors in createSector
            toBeValidated = [userContext.user_arguments[0]]; 
            markValidated(toBeValidated);
            udebug.log('validateProjection complete for', projections[0].domainObject.name);
            callback(null);
            return;
          }
        }
        // report errors and call back user
        if (errors) {
          udebug.log('validateProjection had errors:\n', errors);
          err = new Error(errors);
          err.sqlstate = 'HY000';
        }
      }
      callback(err);
    }
  };


  // validateProjection starts here
  // projection: {domainObject:<constructor>, fields: [field, field], relationships: {field: {projection}, field: {projection}}
  // first check to see if the projection is already validated. If so, we are done.
  // the entire projection including all referenced relationships must be checked because a relationship
  // might have changed since it was last validated.
  // projections will grow at the end as validation checking proceeds
  // construct a new array which will grow as validation checking proceeds
  if (userContext.user_arguments[0].validated) {
    callback(null);
  } else {
    // set up to iteratively validate projection starting with the user parameter
    projections = [this.user_arguments[0]]; // projections will grow at the end as validation proceeds
    index = 0;                              // index into projections for the projection being validated
    offset = 0;                             // the number of fields in previous projections
    errors = '';                            // errors in validation
    mappingIds = [];                        // mapping ids seen so far

    // the projection is not already validated; check to see if the domain object already has its dbTableHandler
    domainObjectMynode = projections[0].domainObject.prototype.mynode;
    if (domainObjectMynode && domainObjectMynode.dbTableHandler) {
      udebug.log('validateProjection with cached tableHandler for', projections[0].domainObject.name);
      validateProjectionOnTableHandler(null, domainObjectMynode.dbTableHandler);
    } else {
      // get the dbTableHandler the hard way
      udebug.log('validateProjection with no tableHandler for', projections[0].domainObject.name);
      getTableHandler(projections[index].domainObject, userContext.session, validateProjectionOnTableHandler);
    }
  }
};

/** Use the projection to find a domain object. This is only valid in a session, not a batch.
 * Multiple operations may be needed to resolve the complete projection.
 * Take the user's projection and see if it has been resolved. For an unresolved projection, 
 * load table mappings for all included domain objects and verify the projection against
 * the resolved table mappings. 
 * Once the projection has been resolved, get the db index to use for the operation,
 * call db session to create a read with projection operation, and execute the operation.
 * The db session will process the projection to populate the result.
 */
exports.UserContext.prototype.findWithProjection = function() {
  var userContext = this;
  var session = userContext.session;
  var dbSession = session.dbSession;
  var projection = userContext.user_arguments[0];
  var keys = userContext.user_arguments[1];
  var dbTableHandler;
  var indexHandler;
  var transactionHandler;

  function findWithProjectionOnResult(err, dbOperation) {
      udebug.log('find.findWithProjectionOnResult');
      var result, values;
      var error = checkOperation(err, dbOperation);
      if (error && dbOperation.result.error.sqlstate !== '02000') {
        if (userContext.session.tx.isActive()) {
          userContext.session.tx.setRollbackOnly();
        }
        userContext.applyCallback(err, null);
      } else {
        if(udebug.is_detail()) { udebug.log('findOnResult returning ', dbOperation.result.value); }
        userContext.applyCallback(null, dbOperation.result.value);      
      }
    }


  function onValidatedProjection(err) {
    if (err) {
      udebug.log('UserContext.onValidProjection err: ', err);
      userContext.applyCallback(err, null);
    } else {
      dbTableHandler = projection.dbTableHandler;
      userContext.dbTableHandler = dbTableHandler;
      keys = userContext.user_arguments[1];
      indexHandler = dbTableHandler.getIndexHandler(keys, true);
      if (indexHandler === null) {
        err = new Error('UserContext.find unable to get an index for ' + dbTableHandler.dbTable.name +
            ' to use with ' + JSON.stringify(keys));
        userContext.applyCallback(err, null);
      } else {
        // create the find operation and execute it
        dbSession = userContext.session.dbSession;
        transactionHandler = dbSession.getTransactionHandler();
        userContext.operation = dbSession.buildReadProjectionOperation(indexHandler, keys, projection,
            transactionHandler, findWithProjectionOnResult);
        if (userContext.execute) {
          transactionHandler.execute([userContext.operation], function() {
            if(udebug.is_detail()) { udebug.log('find transactionHandler.execute callback.'); }
          });
        } else if (typeof(userContext.operationDefinedCallback) === 'function') {
          userContext.operationDefinedCallback(1);
        }
      }
    }
  }
  // findWithProjection starts here
  // validate the projection and construct the sectors
  userContext.validateProjection(onValidatedProjection);
  // the caller will return userContext.promise
};

/** Find the object by key.
 * 
 */
exports.UserContext.prototype.find = function() {
  var userContext = this;
  var tableHandler;
  if (typeof(this.user_arguments[0]) === 'function') {
    userContext.domainObject = true;
  }

  function findOnResult(err, dbOperation) {
    udebug.log('find.findOnResult');
    var result, values;
    var error = checkOperation(err, dbOperation);
    if (error && dbOperation.result.error.sqlstate !== '02000') {
      if (userContext.session.tx.isActive()) {
        userContext.session.tx.setRollbackOnly();
      }
      userContext.applyCallback(err, null);
    } else {
      if(udebug.is_detail()) { udebug.log('findOnResult returning ', dbOperation.result.value); }
      userContext.applyCallback(null, dbOperation.result.value);      
    }
  }

  function findOnTableHandler(err, dbTableHandler) {
    var dbSession, keys, index, transactionHandler;
    if (userContext.clear) {
      // if batch has been cleared, user callback has already been called
      return;
    }
    if (err) {
      userContext.applyCallback(err, null);
    } else {
      userContext.dbTableHandler = dbTableHandler;
      keys = userContext.user_arguments[1];
      index = dbTableHandler.getIndexHandler(keys, true);
      if (index === null) {
        err = new Error('UserContext.find unable to get an index to use for ' + JSON.stringify(keys));
        userContext.applyCallback(err, null);
      } else {
        // create the find operation and execute it
        dbSession = userContext.session.dbSession;
        transactionHandler = dbSession.getTransactionHandler();
        userContext.operation = dbSession.buildReadOperation(index, keys,
            transactionHandler, findOnResult);
        if (userContext.execute) {
          transactionHandler.execute([userContext.operation], function() {
            if(udebug.is_detail()) { udebug.log('find transactionHandler.execute callback.'); }
          });
        } else if (typeof(userContext.operationDefinedCallback) === 'function') {
          userContext.operationDefinedCallback(1);
        }
      }
    }
  }

  // find starts here
  // session.find(projectionOrPrototypeOrTableName, key, callback)
  // validate first two parameters must be defined
  if (userContext.user_arguments[0] === undefined || userContext.user_arguments[1] === undefined) {
    userContext.applyCallback(new Error('User error: find must have at least two arguments.'), null);
  } else {
    if (userContext.user_arguments[0].constructor.name === 'Projection' &&
        typeof userContext.user_arguments[0].constructor.prototype.addRelationship === 'function') {
      // this is a projection
      userContext.findWithProjection();
    } else {
      // get DBTableHandler for prototype/tableName
      getTableHandler(userContext.user_arguments[0], userContext.session, findOnTableHandler);
    }
  }
  return userContext.promise;
};


/** Create a query object.
 * 
 */
exports.UserContext.prototype.createQuery = function() {
  var userContext = this;
  var tableHandler;
  var queryDomainType;

  function createQueryOnTableHandler(err, dbTableHandler) {
    if (err) {
      userContext.applyCallback(err, null);
    } else {
      // create the query domain type and bind it to this session
      queryDomainType = new query.QueryDomainType(userContext.session, dbTableHandler, userContext.domainObject);
      if(udebug.is_detail()) { udebug.log('UserContext.createQuery queryDomainType:', queryDomainType); }
      userContext.applyCallback(null, queryDomainType);
    }
  }

  // createQuery starts here
  // session.createQuery(constructorOrTableName, callback)
  // if the first parameter is a query object then copy the interesting bits and create a new object
  if (this.user_arguments[0].mynode_query_domain_type) {
    // TODO make sure this sessionFactory === other.sessionFactory
    queryDomainType = new query.QueryDomainType(userContext.session);
  }
  // if the first parameter is a table name the query results will be literals
  // if not (constructor or domain object) the query results will be domain objects
  userContext.domainObject = typeof(this.user_arguments[0]) !== 'string';
  // get DBTableHandler for constructor/tableName
  getTableHandler(userContext.user_arguments[0], userContext.session, createQueryOnTableHandler);

  return userContext.promise;
};

/** maximum skip and limit parameters are some large number */
var MAX_SKIP = Math.pow(2, 52);
var MAX_LIMIT = Math.pow(2, 52);

/** Execute a query. 
 * 
 */
exports.UserContext.prototype.executeQuery = function(queryDomainType) {
  var userContext = this;
  var dbSession, transactionHandler, queryType;
  userContext.queryDomainType = queryDomainType;

  // transform query result
  function executeQueryKeyOnResult(err, dbOperation) {
    udebug.log('executeQuery.executeQueryPKOnResult');
    var result, values, resultList;
    var error = checkOperation(err, dbOperation);
    if (error) {
      userContext.applyCallback(error, null);
    } else {
      if (userContext.queryDomainType.mynode_query_domain_type.domainObject) {
        values = dbOperation.result.value;
        result = userContext.queryDomainType.mynode_query_domain_type.dbTableHandler.newResultObject(values);
      } else {
        result = dbOperation.result.value;
      }
      if (result !== null) {
        // TODO: filter in memory if the adapter didn't filter all conditions
        resultList = [result];
      } else {
        resultList = [];
      }
      userContext.applyCallback(null, resultList);      
    }
  }

  // transform query result
  function executeQueryScanOnResult(err, dbOperation) {
    if(udebug.is_detail()) { udebug.log('executeQuery.executeQueryScanOnResult'); }
    var result, values, resultList;
    var error = checkOperation(err, dbOperation);
    if (error) {
      userContext.applyCallback(error, null);
    } else {
      if(udebug.is_detail()) { udebug.log('executeQuery.executeQueryScanOnResult', dbOperation.result.value); }
      // TODO: filter in memory if the adapter didn't filter all conditions
      userContext.applyCallback(null, dbOperation.result.value);      
    }
  }

  // executeScanQuery is used by both index scan and table scan
  var executeScanQuery = function() {
    // validate order, skip, and limit parameters
    var params = userContext.user_arguments[0];
    var orderToUpperCase;
    var order = params.order, skip = params.skip, limit = params.limit;
    var error;
    if (typeof(limit) !== 'undefined') {
      if (limit < 0 || limit > MAX_LIMIT) {
        // limit is out of valid range
        error = new Error('Bad limit parameter \'' + limit + '\'; limit must be >= 0 and <= ' + MAX_LIMIT + '.');
      }
    }
    if (typeof(skip) !== 'undefined') {
      if (skip < 0 || skip > MAX_SKIP) {
        // skip is out of valid range
        error = new Error('Bad skip parameter \'' + skip + '\'; skip must be >= 0 and <= ' + MAX_SKIP + '.');
      } else {
        if (!order) {
          // skip is in range but order is not specified
          error = new Error('Bad skip parameter \'' + skip + '\'; if skip is specified, then order must be specified.');
        }
      }
    }
    if (typeof(order) !== 'undefined') {
      if (typeof(order) !== 'string') {
        error = new Error('Bad order parameter \'' + order + '\'; order must be ignoreCase asc or desc.');
      } else {
        orderToUpperCase = order.toUpperCase();
        if (!(orderToUpperCase === 'ASC' || orderToUpperCase === 'DESC')) {
          error = new Error('Bad order parameter \'' + order + '\'; order must be ignoreCase asc or desc.');
        }
      }
    }
    if (error) {
      userContext.applyCallback(error, null);
    } else {
      dbSession = userContext.session.dbSession;
      transactionHandler = dbSession.getTransactionHandler();
      userContext.operation = dbSession.buildScanOperation(
          queryDomainType, userContext.user_arguments[0], transactionHandler,
          executeQueryScanOnResult);
      // TODO: this currently does not support batching
      transactionHandler.execute([userContext.operation], function() {
        if(udebug.is_detail()) { udebug.log('executeQueryPK transactionHandler.execute callback.'); }
      });
    }
//  if (userContext.execute) {
//  transactionHandler.execute([userContext.operation], function() {
//    if(udebug.is_detail()) udebug.log('find transactionHandler.execute callback.');
//  });
//} else if (typeof(userContext.operationDefinedCallback) === 'function') {
//  userContext.operationDefinedCallback(1);
//}    
  };    

  // executeKeyQuery is used by both primary key and unique key
  var executeKeyQuery = function() {
    // create the find operation and execute it
    dbSession = userContext.session.dbSession;
    transactionHandler = dbSession.getTransactionHandler();
    var dbIndexHandler = queryDomainType.mynode_query_domain_type.queryHandler.candidateIndex.dbIndexHandler;
    var keys = queryDomainType.mynode_query_domain_type.queryHandler.getKeys(userContext.user_arguments[0]);
    userContext.operation = dbSession.buildReadOperation(dbIndexHandler, keys, transactionHandler,
        executeQueryKeyOnResult);
    // TODO: this currently does not support batching
    transactionHandler.execute([userContext.operation], function() {
      if(udebug.is_detail()) { udebug.log('executeQueryPK transactionHandler.execute callback.'); }
    });
//    if (userContext.execute) {
//      transactionHandler.execute([userContext.operation], function() {
//        if(udebug.is_detail()) udebug.log('find transactionHandler.execute callback.');
//      });
//    } else if (typeof(userContext.operationDefinedCallback) === 'function') {
//      userContext.operationDefinedCallback(1);
//    }    
  };
  
  // executeQuery starts here
  // query.execute(parameters, callback)
  udebug.log('QueryDomainType.execute', queryDomainType.mynode_query_domain_type.predicate, 
      'with parameters', userContext.user_arguments[0]);
  // execute the query and call back user
  queryType = queryDomainType.mynode_query_domain_type.queryType;
  switch(queryType) {
  case 0: // primary key
    executeKeyQuery();
    break;

  case 1: // unique key
    executeKeyQuery();
    break;

  case 2: // index scan
    executeScanQuery();
    break;

  case 3: // table scan
    executeScanQuery();
    break;

  default: 
    throw new Error('FatalInternalException: queryType: ' + queryType + ' not supported');
  }
  
  return userContext.promise;
};


/** Persist the object.
 * 
 */
exports.UserContext.prototype.persist = function() {
  var userContext = this;
  var object;

  function persistOnResult(err, dbOperation) {
    udebug.log('persist.persistOnResult');
    // return any error code
    var error = checkOperation(err, dbOperation);
    if (error) {
      if (userContext.session.tx.isActive()) {
        userContext.session.tx.setRollbackOnly();
      }
      userContext.applyCallback(error);
    } else {
      if (dbOperation.result.autoincrementValue) {
        // put returned autoincrement value into object
        userContext.dbTableHandler.setAutoincrement(userContext.values, dbOperation.result.autoincrementValue);
      }
      userContext.applyCallback(null);      
    }
  }

  function persistOnTableHandler(err, dbTableHandler) {
    userContext.dbTableHandler = dbTableHandler;
    if(udebug.is_detail()){  udebug.log('UserContext.persist.persistOnTableHandler ' + err); }
    var transactionHandler;
    var dbSession = userContext.session.dbSession;
    if (userContext.clear) {
      // if batch has been cleared, user callback has already been called
      return;
    }
    if (err) {
      userContext.applyCallback(err);
    } else {
      transactionHandler = dbSession.getTransactionHandler();
      userContext.operation = dbSession.buildInsertOperation(dbTableHandler, userContext.values, transactionHandler,
          persistOnResult);
      if (userContext.execute) {
        transactionHandler.execute([userContext.operation], function() {
          if(udebug.is_detail()) { udebug.log('persist transactionHandler.execute callback.'); }
        });
      } else if (typeof(userContext.operationDefinedCallback) === 'function') {
        userContext.operationDefinedCallback(1);
      }
    }
  }

  // persist starts here
  if (userContext.required_parameter_count === 2) {
    // persist(object, callback)
    userContext.values = userContext.user_arguments[0];
  } else if (userContext.required_parameter_count === 3) {
    // persist(tableNameOrConstructor, values, callback)
    userContext.values = userContext.user_arguments[1];
  } else {
    throw new Error(
        'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count);
  }
  // get DBTableHandler for table indicator (domain object, constructor, or table name)
  getTableHandler(userContext.user_arguments[0], userContext.session, persistOnTableHandler);
  return userContext.promise;
};

/** Save the object. If the row already exists, overwrite non-pk columns.
 * 
 */
exports.UserContext.prototype.save = function() {
  var userContext = this;
  var tableHandler, object, indexHandler;

  function saveOnResult(err, dbOperation) {
    // return any error code
    var error = checkOperation(err, dbOperation);
    if (error) {
      if (userContext.session.tx.isActive()) {
        userContext.session.tx.setRollbackOnly();
      }
      userContext.applyCallback(error);
    } else {
      userContext.applyCallback(null);      
    }
  }

  function saveOnTableHandler(err, dbTableHandler) {
    var transactionHandler;
    var dbSession = userContext.session.dbSession;
    if (userContext.clear) {
      // if batch has been cleared, user callback has already been called
      return;
    }
    if (err) {
      userContext.applyCallback(err);
    } else {
      transactionHandler = dbSession.getTransactionHandler();
      indexHandler = dbTableHandler.getIndexHandler(userContext.values);
      if (!indexHandler.dbIndex.isPrimaryKey) {
        userContext.applyCallback(
            new Error('Illegal argument: parameter of save must include all primary key columns.'));
        return;
      }
      userContext.operation = dbSession.buildWriteOperation(indexHandler, userContext.values, transactionHandler,
          saveOnResult);
      if (userContext.execute) {
        transactionHandler.execute([userContext.operation], function() {
        });
      } else if (typeof(userContext.operationDefinedCallback) === 'function') {
        userContext.operationDefinedCallback(1);
      }
    }
  }

  // save starts here

  if (userContext.required_parameter_count === 2) {
    // save(object, callback)
    userContext.values = userContext.user_arguments[0];
  } else if (userContext.required_parameter_count === 3) {
    // save(tableNameOrConstructor, values, callback)
    userContext.values = userContext.user_arguments[1];
  } else {
    throw new Error(
        'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count);
  }
  // get DBTableHandler for table indicator (domain object, constructor, or table name)
  getTableHandler(userContext.user_arguments[0], userContext.session, saveOnTableHandler);
  return userContext.promise;
};

/** Update the object.
 * 
 */
exports.UserContext.prototype.update = function() {
  var userContext = this;
  var tableHandler, object, indexHandler;

  function updateOnResult(err, dbOperation) {
    // return any error code
    var error = checkOperation(err, dbOperation);
    if (error) {
      if (userContext.session.tx.isActive()) {
        userContext.session.tx.setRollbackOnly();
      }
      userContext.applyCallback(error);
    } else {
      userContext.applyCallback(null);      
    }
  }

  function updateOnTableHandler(err, dbTableHandler) {
    var transactionHandler;
    var dbSession = userContext.session.dbSession;
    if (userContext.clear) {
      // if batch has been cleared, user callback has already been called
      return;
    }
    if (err) {
      userContext.applyCallback(err);
    } else {
      transactionHandler = dbSession.getTransactionHandler();
      indexHandler = dbTableHandler.getIndexHandler(userContext.keys);
      // for variant update(object, callback) the object must include all primary keys
      if (userContext.required_parameter_count === 2 && !indexHandler.dbIndex.isPrimaryKey) {
        userContext.applyCallback(
            new Error('Illegal argument: parameter of update must include all primary key columns.'));
        return;
      }
      userContext.operation = dbSession.buildUpdateOperation(indexHandler, userContext.keys,
          userContext.values, transactionHandler, updateOnResult);
      if (userContext.execute) {
        transactionHandler.execute([userContext.operation], function() {
        });
      } else if (typeof(userContext.operationDefinedCallback) === 'function') {
        userContext.operationDefinedCallback(1);
      }
    }
  }

  // update starts here

  if (userContext.required_parameter_count === 2) {
    // update(object, callback)
    userContext.keys = userContext.user_arguments[0];
    userContext.values = userContext.user_arguments[0];
  } else if (userContext.required_parameter_count === 4) {
    // update(tableNameOrConstructor, keys, values, callback)
    userContext.keys = userContext.user_arguments[1];
    userContext.values = userContext.user_arguments[2];
  } else {
    throw new Error(
        'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count);
  }
  // get DBTableHandler for table indicator (domain object, constructor, or table name)
  getTableHandler(userContext.user_arguments[0], userContext.session, updateOnTableHandler);
  return userContext.promise;
};

/** Load the object.
 * 
 */
exports.UserContext.prototype.load = function() {
  var userContext = this;
  var tableHandler;

  function loadOnResult(err, dbOperation) {
    udebug.log('load.loadOnResult');
    var result, values;
    var error = checkOperation(err, dbOperation);
    if (error) {
      if (userContext.session.tx.isActive()) {
        userContext.session.tx.setRollbackOnly();
      }
      userContext.applyCallback(err);
      return;
    }
    values = dbOperation.result.value;
    // apply the values to the parameter domain object
    userContext.dbTableHandler.setFields(userContext.user_arguments[0], values);
    userContext.applyCallback(null);      
  }

  function loadOnTableHandler(err, dbTableHandler) {
    var dbSession, keys, index, transactionHandler;
    if (userContext.clear) {
      // if batch has been cleared, user callback has already been called
      return;
    }
    if (err) {
      userContext.applyCallback(err);
    } else {
      userContext.dbTableHandler = dbTableHandler;
      // the domain object must provide PRIMARY or unique key
      keys = userContext.user_arguments[0];
      // ask getIndexHandler for only unique key indexes
      index = dbTableHandler.getIndexHandler(keys, true);
      if (index === null) {
        err = new Error('Illegal argument: load unable to get a unique index to use for ' + JSON.stringify(keys));
        userContext.applyCallback(err);
      } else {
        // create the load operation and execute it
        dbSession = userContext.session.dbSession;
        transactionHandler = dbSession.getTransactionHandler();
        userContext.operation = dbSession.buildReadOperation(index, keys, transactionHandler,
            loadOnResult);
        if (userContext.execute) {
          transactionHandler.execute([userContext.operation], function() {
            if(udebug.is_detail()) { udebug.log('load transactionHandler.execute callback.'); }
          });
        } else if (typeof(userContext.operationDefinedCallback) === 'function') {
          userContext.operationDefinedCallback(1);
        }
      }
    }
  }

  // load starts here
  // session.load(instance, callback)
  // get DBTableHandler for instance constructor
  if (typeof(userContext.user_arguments[0].mynode) !== 'object') {
    userContext.applyCallback(new Error('Illegal argument: load requires a mapped domain object.'));
    return;
  }
  var ctor = userContext.user_arguments[0].mynode.constructor;
  getTableHandler(ctor, userContext.session, loadOnTableHandler);
  return userContext.promise;
};

/** Remove the object.
 * 
 */
exports.UserContext.prototype.remove = function() {
  var userContext = this;
  var tableHandler, object;

  function removeOnResult(err, dbOperation) {
    udebug.log('remove.removeOnResult');
    // return any error code plus the original user object
    var error = checkOperation(err, dbOperation);
    if (error) {
      if (userContext.session.tx.isActive()) {
        userContext.session.tx.setRollbackOnly();
      }
      userContext.applyCallback(error);
    } else {
      var result = dbOperation.result.value;
      userContext.applyCallback(null);
    }
  }

  function removeOnTableHandler(err, dbTableHandler) {
    var transactionHandler, object, dbIndexHandler;
    var dbSession = userContext.session.dbSession;
    if (userContext.clear) {
      // if batch has been cleared, user callback has already been called
      return;
    }
    if (err) {
      userContext.applyCallback(err);
    } else {
      dbIndexHandler = dbTableHandler.getIndexHandler(userContext.keys, true);
      if (dbIndexHandler === null) {
        err = new Error('UserContext.remove unable to get an index to use for ' + JSON.stringify(userContext.keys));
        userContext.applyCallback(err);
      } else {
        transactionHandler = dbSession.getTransactionHandler();
        userContext.operation = dbSession.buildDeleteOperation(
            dbIndexHandler, userContext.keys, transactionHandler, removeOnResult);
        if (userContext.execute) {
          transactionHandler.execute([userContext.operation], function() {
            if(udebug.is_detail()) { udebug.log('remove transactionHandler.execute callback.'); }
          });
        } else if (typeof(userContext.operationDefinedCallback) === 'function') {
          userContext.operationDefinedCallback(1);
        }
      }
    }
  }

  // remove starts here

  if (userContext.required_parameter_count === 2) {
    // remove(object, callback)
    userContext.keys = userContext.user_arguments[0];
  } else if (userContext.required_parameter_count === 3) {
    // remove(tableNameOrConstructor, values, callback)
    userContext.keys = userContext.user_arguments[1];
  } else {
    throw new Error(
        'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count);
  }
  // get DBTableHandler for table indicator (domain object, constructor, or table name)
  getTableHandler(userContext.user_arguments[0], userContext.session, removeOnTableHandler);
  return userContext.promise;
};

/** Get Mapping
 * 
 */
exports.UserContext.prototype.getMapping = function() {
  var userContext = this;
  function getMappingOnTableHandler(err, dbTableHandler) {
    if (err) {
      userContext.applyCallback(err, null);
      return;
    }
    var mapping = dbTableHandler.resolvedMapping;
    userContext.applyCallback(null, mapping);
  }
  // getMapping starts here
  getTableHandler(userContext.user_arguments[0], userContext.session, getMappingOnTableHandler);  
  return userContext.promise;
};

/** Execute a batch
 * 
 */
exports.UserContext.prototype.executeBatch = function(operationContexts) {
  var userContext = this;
  userContext.operationContexts = operationContexts;
  userContext.numberOfOperations = operationContexts.length;
  userContext.numberOfOperationsDefined = 0;

  // all operations have been executed and their user callbacks called
  // now call the Batch.execute callback
  var executeBatchOnExecute = function(err) {
    userContext.applyCallback(err);
  };

  // wait here until all operations have been defined
  // if operations are not yet defined, the onTableHandler callback
  // will call this function after the operation is defined
  var executeBatchOnOperationDefined = function(definedOperationCount) {
    userContext.numberOfOperationsDefined += definedOperationCount;
    if(udebug.is_detail()) { 
      udebug.log('UserContext.executeBatch expecting', userContext.numberOfOperations, 
                'operations with', userContext.numberOfOperationsDefined, 'already defined.');
    }
    if (userContext.numberOfOperationsDefined === userContext.numberOfOperations) {
      var operations = [];
      // collect all operations from the operation contexts
      userContext.operationContexts.forEach(function(operationContext) {
        operations.push(operationContext.operation);
      });
      // execute the batch
      var transactionHandler;
      var dbSession;
      dbSession = userContext.session.dbSession;
      transactionHandler = dbSession.getTransactionHandler();
      transactionHandler.execute(operations, executeBatchOnExecute);
    }
  };

  // executeBatch starts here
  // if no operations in the batch, just call the user callback
  if (operationContexts.length == 0) {
    executeBatchOnExecute(null);
  } else {
    // make sure all operations are defined
    operationContexts.forEach(function(operationContext) {
      // is the operation already defined?
      if (typeof(operationContext.operation) !== 'undefined') {
        userContext.numberOfOperationsDefined++;
      } else {
        // the operation has not been defined yet; set a callback for when the operation is defined
        operationContext.operationDefinedCallback = executeBatchOnOperationDefined;
      }
    });
    // now execute the operations
    executeBatchOnOperationDefined(0);
  }
  return userContext.promise;
};

/** Commit an active transaction. 
 * 
 */
exports.UserContext.prototype.commit = function() {
  var userContext = this;

  var commitOnCommit = function(err) {
    udebug.log('UserContext.commitOnCommit.');
    userContext.session.tx.setState(userContext.session.tx.idle);
    userContext.applyCallback(err);
  };

  // commit begins here
  if (userContext.session.tx.isActive()) {
    udebug.log('UserContext.commit tx is active.');
    userContext.session.dbSession.commit(commitOnCommit);
  } else {
    userContext.applyCallback(
        new Error('Fatal Internal Exception: UserContext.commit with no active transaction.'));
  }
  return userContext.promise;
};


/** Roll back an active transaction. 
 * 
 */
exports.UserContext.prototype.rollback = function() {
  var userContext = this;

  var rollbackOnRollback = function(err) {
    udebug.log('UserContext.rollbackOnRollback.');
    userContext.session.tx.setState(userContext.session.tx.idle);
    userContext.applyCallback(err);
  };

  // rollback begins here
  if (userContext.session.tx.isActive()) {
    udebug.log('UserContext.rollback tx is active.');
    var transactionHandler = userContext.session.dbSession.getTransactionHandler();
    transactionHandler.rollback(rollbackOnRollback);
  } else {
    userContext.applyCallback(
        new Error('Fatal Internal Exception: UserContext.rollback with no active transaction.'));
  }
  return userContext.promise;
};


/** Open a session. Allocate a slot in the session factory sessions array.
 * Call the DBConnectionPool to create a new DBSession.
 * Wrap the DBSession in a new Session and return it to the user.
 * This function is called by both mynode.openSession (without a session factory)
 * and SessionFactory.openSession (with a session factory).
 */
exports.UserContext.prototype.openSession = function() {
  var userContext = this;

  var openSessionOnSession = function(err, dbSession) {
    if (err) {
      userContext.applyCallback(err, null);
    } else {
      userContext.session = new apiSession.Session(userContext.session_index, userContext.session_factory, dbSession);
      userContext.session_factory.sessions[userContext.session_index] = userContext.session;
      userContext.applyCallback(err, userContext.session);
    }
  };

  var openSessionOnSessionFactory = function(err, factory) {
    if (err) {
      userContext.applyCallback(err, null);
    } else {
      userContext.session_factory = factory;
      // allocate a new session slot in sessions
      userContext.session_index = userContext.session_factory.allocateSessionSlot();
      // get a new DBSession from the DBConnectionPool
      userContext.session_factory.dbConnectionPool.getDBSession(userContext.session_index, 
          openSessionOnSession);
    }
  };
  
  // openSession starts here
  if (userContext.session_factory) {
    openSessionOnSessionFactory(null, userContext.session_factory);
  } else {
    if(udebug.is_detail()) { udebug.log('openSession for', util.inspect(userContext)); }
    // properties might be null, a name, or a properties object
    userContext.user_arguments[0] = resolveProperties(userContext.user_arguments[0]);
    getSessionFactory(userContext, userContext.user_arguments[0], userContext.user_arguments[1], 
        openSessionOnSessionFactory);
  }
  return userContext.promise;
};

/** Close a session. Close the dbSession which might put the underlying connection
 * back into the connection pool. Then, remove the session from the session factory's
 * open connections.
 * 
 */
exports.UserContext.prototype.closeSession = function() {
  var userContext = this;

  var closeSessionOnDBSessionClose = function(err) {
    // now remove the session from the session factory's open connections
    userContext.session_factory.closeSession(userContext.session.index);
    // mark this session as unusable
    userContext.session.closed = true;
    userContext.applyCallback(err);
  };
  // first, close the dbSession
  userContext.session.dbSession.close(closeSessionOnDBSessionClose);
  return userContext.promise;
};


/** Close all open SessionFactories
 *
 */
exports.UserContext.prototype.closeAllOpenSessionFactories = function() {
  var userContext, openFactories, nToClose;

  userContext   = this;
  openFactories = mynode.getOpenSessionFactories();
  nToClose      = openFactories.length;

  function onFactoryClose() {
    nToClose--;
    if(nToClose === 0) {
      userContext.applyCallback(null);
    }
  }

  if(nToClose > 0) {
    while(openFactories[0]) {
      openFactories[0].close(onFactoryClose);
      openFactories.shift();
    }
  } else {
    userContext.applyCallback(null);
  }
  return userContext.promise;
};


/** Complete the user function by calling back the user with the results of the function.
 * Apply the user callback using the current arguments and the extra parameters from the original function.
 * Create the args for the callback by copying the current arguments to this function. Then, copy
 * the extra parameters from the original function. Finally, call the user callback.
 * If there is no user callback, and there is an error (first argument to applyCallback)
 * throw the error.
 */
exports.UserContext.prototype.applyCallback = function(err, result) {
  if (arguments.length !== this.returned_parameter_count) {
    throw new Error(
        'Fatal internal exception: wrong parameter count ' + arguments.length +' for UserContext applyCallback' + 
        '; expected ' + this.returned_parameter_count);
  }
  // notify (either fulfill or reject) the promise
  if (err) {
    if(udebug.is_detail()) { udebug.log('UserContext.applyCallback.reject', err); }
    this.promise.reject(err);
  } else {
    if(udebug.is_detail()) { udebug.log('UserContext.applyCallback.fulfill', result); }
    this.promise.fulfill(result);
  }
  if (typeof(this.user_callback) === 'undefined') {
    if(udebug.is_detail()) udebug.log('UserContext.applyCallback with no user_callback.');
    return;
  }
  var args = [];
  var i, j;
  for (i = 0; i < arguments.length; ++i) {
    args.push(arguments[i]);
  }
  for (j = this.required_parameter_count; j < this.user_arguments.length; ++j) {
    args.push(this.user_arguments[j]);
  }
  this.user_callback.apply(null, args);
};

exports.Promise = Promise;